
'''
策略逻辑如下: 

1、选择待匹配股票: 每周第一个交易日从全A股票中剔除高风险股, 再计算其最近一周涨跌幅, 挑选涨幅最高的一只股票。

2、计算相关性: 准备该股票上上周的K线形态数据（记为data1）和当前的所有股票最近一周的K线形态数据（记为data2）, 计算data1和data2的相关性。

3、买入相似股票: 剔除低相关性的股票, 在剩余股票中平均买入相关性最高的N只股票。

4、每周轮换: 新的一周, 从步骤1循环运行。
'''
# coding=utf-8
from __future__ import print_function, absolute_import
from gm.api import *
import datetime
import pandas as pd
import copy

"""
形态匹配策略
每周第一个交易日选取最近一周表现最好的一只股票, 再以这只股票上上周的K线形态去匹配当前的所有股票, 
剔除低相关性的股票, 在剩余股票中平均买入相关性最高的N只股票, 每周轮换。
"""

def init(context):
    # 形态数量, 即带匹配的股票数
    context.form_num = 1
    # 每个形态匹配的数量
    context.matching_num = 10
    # 涨幅区间时间,天
    context.periods_time = 5
    # 相关性阈值
    context.similarity_threshold = 0.8
    # 每周定时任务
    schedule(schedule_func=algo, date_rule='1w', time_rule='09:31:00')


def algo(context):
    # 获取全A股票（剔除停牌, ST, 次新股等）
    all_stocks,all_stocks_str = get_normal_stocks(context.now,new_days=365)
    context.all_security = all_stocks_str
    # 涨幅区间的开始时间
    return_start_date = get_previous_N_trading_date(context.now,counts=context.periods_time+2)
    # 涨幅区间的结束时间, 即上一个交易日
    return_end_date = get_previous_trading_date(exchange='SZSE', date=context.now)
    # 选取历史M天涨幅最高的N只股票
    close = history_new(security=all_stocks_str,frequency='1d',start_time=return_start_date,end_time=return_end_date,fields='eob,symbol,close',skip_suspended=True,fill_missing=None,adjust=ADJUST_POST,df=True)
    return_rate = (close.iloc[-1,:]/close.iloc[0,:]-1).sort_values(ascending = False)[:context.form_num]
    best_symbols = ','.join(list(return_rate.index))
    # 形态匹配
    # 形态的开始时间
    form_start_date = get_previous_N_trading_date(context.now,counts=context.periods_time+context.periods_time+1)
    to_buy = form_matching(context,security=best_symbols,start_date=form_start_date,end_date=return_start_date,simi_threshold=context.similarity_threshold,matching_num=context.matching_num)
    print('{}, 带买入股票: {}'.format(context.now,to_buy))

   ## 股票交易
    # 获取持仓
    positions = context.account().positions()
    # 卖出不在to_buy中的持仓(跌停不卖出)
    for position in positions:
        symbol = position['symbol']
        if symbol not in to_buy:
            lower_limit = get_history_instruments(symbol, fields='lower_limit', start_date=context.now, end_date=context.now, df=True)
            new_price = history(symbol=symbol, frequency='60s', start_time=context.now, end_time=context.now, fields='close', df=True)
            if symbol not in to_buy and (len(new_price)==0 or len(lower_limit)==0 or lower_limit['lower_limit'][0]!=round(new_price['close'][0],2)):
                # new_price为空时, 是开盘后无成交的现象, 此处忽略该情况, 可能会包含涨跌停的股票
                order_target_percent(symbol=symbol, percent=0, order_type=OrderType_Market, position_side=PositionSide_Long)
    # 买入股票（涨停不买入）
    for symbol in to_buy:
        upper_limit = get_history_instruments(symbol, fields='upper_limit', start_date=context.now, end_date=context.now, df=True)
        new_price = history(symbol=symbol, frequency='60s', start_time=context.now, end_time=context.now, fields='close', df=True)
        if len(new_price)==0 or len(upper_limit)==0 or upper_limit['upper_limit'][0]!=round(new_price['close'][0],2):
            # new_price为空时, 是开盘后无成交的现象, 此处忽略该情况, 可能会包含涨跌停的股票
            order_target_percent(symbol=symbol, percent=1/len(to_buy), order_type=OrderType_Market, position_side=PositionSide_Long)


def form_matching(context,security,start_date,end_date,simi_threshold,matching_num=1):
    """
    形态匹配
    :param security:待匹配的股票, str
    :param start_date: 形态的开始时间
    :param end_date:形态的结束时间
    :param simi_threshold: 形似度的阈值
    :param maatching_num:每只股票的匹配数量, 默认为1
    """
    # 形态匹配的区间天数
    form_days = len(get_trading_dates(exchange='SZSE', start_date=start_date, end_date=end_date))
    # 形态开始时间的上一个交易日
    last_form_days = get_previous_trading_date(exchange='SZSE', date=start_date)
    ## 获取待匹配股票的历史形态数据(每日涨跌幅、每日振幅、每日成交量变化比率)
    # 每日涨跌幅
    close = history_new(security=security,frequency='1d',start_time=last_form_days,end_time=end_date,fields='eob,symbol,close',skip_suspended=True,fill_missing='Last',adjust=ADJUST_POST,df=True)
    d_return = (close/close.shift(1)-1).iloc[1:,:]
    # 每日振幅
    high = history_new(security=security,frequency='1d',start_time=start_date,end_time=end_date,fields='eob,symbol,high',skip_suspended=True,fill_missing='NaN',adjust=ADJUST_POST,df=True)
    low = history_new(security=security,frequency='1d',start_time=start_date,end_time=end_date,fields='eob,symbol,low',skip_suspended=True,fill_missing='NaN',adjust=ADJUST_POST,df=True)
    d_amplitude = high/low-1
    # 每日成交量变化比率
    volume = history_new(security=security,frequency='1d',start_time=last_form_days,end_time=end_date,fields='eob,symbol,volume',skip_suspended=True,fill_missing='NaN',adjust=ADJUST_POST,df=True)
    d_volume_rate = (volume/volume.shift(1)-1).iloc[1:,:]

    ## 获取当前所有股票（剔除停牌, ST, 次新股等）的最新形态数据
    # 最新形态数据的开始时间
    new_start_date = get_previous_N_trading_date(context.now,counts=form_days+1)
    # 最新形态数据的结束时间, 即上一个交易日
    new_end_date = get_previous_trading_date(exchange='SZSE', date=context.now)
    # 最新形态数据开始的上一个交易日
    last_new_start_date = get_previous_trading_date(exchange='SZSE', date=new_start_date)
    # 每日涨跌幅
    new_close = history_new(security=context.all_security,frequency='1d',start_time=last_new_start_date,end_time=new_end_date,fields='eob,symbol,close',skip_suspended=True,fill_missing=None,adjust=ADJUST_POST,df=True)
    new_d_return = (new_close/new_close.shift(1)-1).iloc[1:,:]
    # 每日振幅
    new_high = history_new(security=context.all_security,frequency='1d',start_time=new_start_date,end_time=new_end_date,fields='eob,symbol,high',skip_suspended=True,fill_missing=None,adjust=ADJUST_POST,df=True)
    new_low = history_new(security=context.all_security,frequency='1d',start_time=new_start_date,end_time=new_end_date,fields='eob,symbol,low',skip_suspended=True,fill_missing=None,adjust=ADJUST_POST,df=True)
    new_d_amplitude = new_high/new_low-1
    # 每日成交量变化比率
    new_volume = history_new(security=context.all_security,frequency='1d',start_time=last_new_start_date,end_time=new_end_date,fields='eob,symbol,volume',skip_suspended=True,fill_missing=None,adjust=ADJUST_POST,df=True)
    new_d_volume_rate = (new_volume/new_volume.shift(1)-1).iloc[1:,:]
    # 计算相关性
    high_simi_security = []
    for code in security.split(','):
        # 每日收益相关性
        df0 = copy.deepcopy(new_d_return)
        if len(d_return.loc[:,code])<len(df0):continue
        df0.loc[:,'base_form'] = d_return.loc[:,code].values
        simi_return = df0.corr(method ='pearson').loc[:,'base_form']
        simi_return = simi_return[:len(simi_return)-1]
        # # 每日振幅相关性
        df1 = copy.deepcopy(new_d_amplitude)
        if len(d_amplitude.loc[:,code])<len(df1):continue
        df1.loc[:,'base_form'] = d_amplitude.loc[:,code].values
        simi_amplitude = df1.corr(method ='pearson').loc[:,'base_form']
        simi_amplitude = simi_amplitude[:len(simi_amplitude)-1]
        # # 每日成交量变化比率相关性
        df2 = copy.deepcopy(new_d_volume_rate)
        if len(d_volume_rate.loc[:,code])<len(df2):continue
        df2.loc[:,'base_form'] = d_volume_rate.loc[:,code].values
        simi_volume_rate = df2.corr(method ='pearson').loc[:,'base_form']
        simi_volume_rate = simi_volume_rate[:len(simi_volume_rate)-1]
        # 计算平均相似度
        simi = (simi_return+simi_amplitude+simi_volume_rate)/3
        # 过滤相似度阈值
        simi = simi[simi>=context.similarity_threshold]
        if len(simi)>0:
            # 保留股票
            if matching_num==1:
                high_simi_security.append(simi.idxmax())
            else:
                high_simi_security = high_simi_security+list((simi.sort_values(ascending=False)[:matching_num]).index)
    return high_simi_security


def get_previous_N_trading_date(date,counts=1,exchanges='SHSE'):
    """
    获取end_date前N个交易日,end_date为datetime格式, 包括date日期
    :param date: 目标日期
    :param counts: 历史回溯天数, 默认为1, 即前一天
    """
    if isinstance(date,str) and len(date)>10:
        date = datetime.datetime.strptime(date,'%Y-%m-%d %H:%M:%S')
    if isinstance(date,str) and len(date)==10:
        date = datetime.datetime.strptime(date,'%Y-%m-%d')
    previous_N_trading_date = get_trading_dates(exchange=exchanges, start_date=date-datetime.timedelta(days=max(counts+30,counts*2)), end_date=date)[-counts]
    return previous_N_trading_date


def get_normal_stocks(date,new_days=365):
    """
    获取目标日期date的A股代码（剔除停牌股、ST股、次新股（365天））
    :param date: 目标日期
    :param new_days:新股上市天数, 默认为365天
    """
    if isinstance(date,str) and len(date)==10:
        date = datetime.datetime.strptime(date,"%Y-%m-%d")
    elif isinstance(date,str) and len(date)>10:
        date = datetime.datetime.strptime(date,"%Y-%m-%d %H:%M:%S")
    # 先剔除退市股、次新股和B股
    df_code = get_instrumentinfos(sec_types=SEC_TYPE_STOCK, fields='symbol, listed_date, delisted_date', df=True)
    all_stocks = [code for code in df_code[(df_code['listed_date']<=date-datetime.timedelta(days=new_days))&(df_code['delisted_date']>date)].symbol.to_list() if code[:6]!='SHSE.9' and code[:6]!='SZSE.2']
    # 再剔除当前的停牌股和ST股
    history_ins = get_history_instruments(symbols=all_stocks, start_date=date, end_date=date, fields='symbol,sec_level, is_suspended', df=True)
    all_stocks = list(history_ins[(history_ins['sec_level']==1) & (history_ins['is_suspended']==0)]['symbol'])
    all_stocks_str = ','.join(all_stocks)
    return all_stocks,all_stocks_str


def history_new(security,frequency,start_time,end_time,fields,skip_suspended=True,fill_missing=None,adjust=ADJUST_POST,df=True):
    # 分区间获取数据（以避免超出数据限制）(start_time和end_date为字符串,fields需包含eob和symbol,单字段)
    Data = pd.DataFrame()
    if frequency=='1d':
        trading_date = get_trading_dates(exchange='SZSE', start_date=start_time, end_date=end_time)
    else:
        trading_date = history('SHSE.000300', frequency=frequency, start_time=start_time, end_time=end_time, fields='eob', skip_suspended=skip_suspended, fill_missing=fill_missing, adjust=adjust, df=df)
        trading_date = trading_date['eob']
    space = 20
    if len(trading_date)<=space:
        Data = history(security, frequency=frequency, start_time=start_time, end_time=end_time, fields=fields, skip_suspended=skip_suspended, fill_missing=fill_missing, adjust=adjust, df=df)
    else:
        for n in range(int(np.floor(len(trading_date)/space))):
            start = n*space
            end = start+space-1
            if end>=len(trading_date):
                data = history(security, frequency=frequency, start_time=trading_date[start], end_time=trading_date[-1], fields=fields, skip_suspended=skip_suspended, fill_missing=fill_missing, adjust=adjust, df=df)
            else:
                data = history(security, frequency=frequency, start_time=trading_date[start], end_time=trading_date[end], fields=fields, skip_suspended=skip_suspended, fill_missing=fill_missing, adjust=adjust, df=df)
            if len(data)>=33000:
                print('请检查返回数据量, 可能超过系统限制, 缺少数据！！！！！！！！！！')
            Data = pd.concat([Data,data])    
    Data.drop_duplicates(keep='first',inplace=True)
    Data = Data.set_index(['eob','symbol'])
    Data = Data.unstack()
    Data.columns = Data.columns.droplevel(level=0)
    return Data


# 查看最终的回测结果
def on_backtest_finished(context, indicator):
    print(indicator)


if __name__ == '__main__':
    '''
        strategy_id策略ID, 由系统生成
        filename文件名, 请与本文件名保持一致
        mode运行模式, 实时模式:MODE_LIVE回测模式:MODE_BACKTEST
        token绑定计算机的ID, 可在系统设置-密钥管理中生成
        backtest_start_time回测开始时间
        backtest_end_time回测结束时间
        backtest_adjust股票复权方式, 不复权:ADJUST_NONE前复权:ADJUST_PREV后复权:ADJUST_POST
        backtest_initial_cash回测初始资金
        backtest_commission_ratio回测佣金比例
        backtest_slippage_ratio回测滑点比例
    '''
    run(strategy_id='6eece943-915e-11ec-9103-f46b8c02346f',
        filename='main.py',
        mode=MODE_BACKTEST,
        token='47ca47f849b3a0f66ec0f7013bb56bb667d63a70',
        backtest_start_time='2021-01-01 08:00:00',
        backtest_end_time='2022-02-23 16:00:00',
        backtest_adjust=ADJUST_PREV,
        backtest_initial_cash=500000,
        backtest_commission_ratio=0.0016,
        backtest_slippage_ratio=0)
